
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;

namespace ProgrammingLanguageNr1
{
	public class Tokenizer
	{
		public Tokenizer (TextReader textReader, ErrorHandler errorHandler)
		{
			Assert.IsNotNull(textReader);
			
			m_errorHandler = errorHandler;
			
			m_textReader = textReader;
			readNextChar();
		}
		
		public List<Token> getTokens() {
			if (!m_processed) {
				process();	
			}
			return m_tokens;
		}
		
		private void process() {
			Assert.IsFalse(m_processed);
			
			m_endOfFile = false;
			m_currentLine = 1;
			m_currentPosition = 0;
			
			Token t;
			
			do {
				t = readNextToken();
				t.LineNr = m_currentLine;
				t.LinePosition = m_currentTokenStartPosition;
				m_currentTokenStartPosition = m_currentPosition;
				
				m_tokens.Add(t);
				
				//Console.WriteLine(t.getTokenType().ToString() + "\t" + t.getTokenString());
				
			} while(t.getTokenType() != Token.TokenType.EOF);
			
			m_processed = true;
			m_textReader.Close();
		}
		
		private Token readNextToken() {
			
			while (!m_endOfFile) {
				
				switch(m_currentChar) {
				case ' ': case '\t':
					readNextChar();
					continue;
					
				case '#':
					COMMENT();
					continue;
					
				case '\n':
					return NEW_LINE();
					
				case '+': case '-': case '*': case '/': case '<': 
				case '>': case '=': case '!': case '&': case '|':
					return OPERATOR();
					
				case '(':
					return PARANTHESIS_LEFT();
					
				case ')':
					return PARANTHESIS_RIGHT();
					
				case '{':
					return BLOCK_BEGIN();
					
				case '}':
					return BLOCK_END();
					
				case '\"':
					return QUOTED_STRING();
					
				case ',':
					return COMMA();
					
				default:
					if( isLETTER() ) {
						return NAME();
					}
					else if ( isDIGIT() ) {
						return NUMBER(false);
					}
					else {
						m_errorHandler.errorOccured(
							"Unrecognized character found: \'" + 
							m_currentChar + 
							"\'",
							Error.ErrorType.SYNTAX,
							m_currentLine, 
							m_currentPosition);
						// Try to recover:
						readNextChar();
						continue;
					}
				}
				
			}
			
			return new Token(Token.TokenType.EOF, "<EOF>");
		}
		
		private void COMMENT() {
			while(m_currentChar != '\n' && m_currentChar != '\0') {
				// just throw away comments
				readNextChar();
			}
		}
		
		private Token COMMA() {
			readNextChar();
			return new Token(Token.TokenType.COMMA, ",");
		}
		
		private Token NEW_LINE() {
			while(m_currentChar == '\n') { // make several new-lines into a single one
				m_currentLine++;
				m_currentPosition = 0;
				readNextChar();
			}
			return new Token(Token.TokenType.NEW_LINE, "<NEW_LINE>");
		}
		
		private Token OPERATOR ()
		{
			StringBuilder tokenString = new StringBuilder ();
			tokenString.Append (m_currentChar);
			
			char firstChar = m_currentChar;
			readNextChar ();
			
			if (firstChar == '-' && isDIGIT()) {
				// Negative number!
				return NUMBER(true);
			}
			else if ( (firstChar == '<' || firstChar == '>') && m_currentChar == '=') {
				tokenString.Append ('=');
				readNextChar();
			}
			else if ( firstChar == '=') {
				if( m_currentChar == '=' ) {
					tokenString.Append ('=');
				} else {
					return ASSIGNMENT();
				}
				readNextChar();
			}
			else if ( firstChar == '!' && m_currentChar == '=') {
				tokenString.Append ('=');
				readNextChar();
			}
			else if ( firstChar == '&' && m_currentChar == '&') {
				tokenString.Append ('&');
				readNextChar();
			}
			else if ( firstChar == '|' && m_currentChar == '|') {
				tokenString.Append ('|');
				readNextChar();
			}
			
			return new Token(Token.TokenType.OPERATOR, tokenString.ToString());
		}
		
		private Token PARANTHESIS_LEFT() {
			readNextChar();
			return new Token(Token.TokenType.PARANTHESIS_LEFT, "(");
		}
		
		private Token PARANTHESIS_RIGHT() {
			readNextChar();
			return new Token(Token.TokenType.PARANTHESIS_RIGHT, ")");
		}
		
		private Token BLOCK_BEGIN() {
			readNextChar();
			return new Token(Token.TokenType.BLOCK_BEGIN, "{");
		}
		
		private Token BLOCK_END() {
			readNextChar();
			return new Token(Token.TokenType.BLOCK_END, "}");
		}
		
		private Token QUOTED_STRING() {
			StringBuilder tokenString = new StringBuilder();
			readNextChar();
			while( m_currentChar != '\"' ) {
				tokenString.Append(m_currentChar);
				readNextChar();
			} 
			readNextChar();
			return new Token(Token.TokenType.QUOTED_STRING, tokenString.ToString());
		}
		
		private Token ASSIGNMENT() {
			readNextChar();
			return new Token(Token.TokenType.ASSIGNMENT, "=");
		}
		
		private Token NAME() {
			StringBuilder tokenString = new StringBuilder();
			do {
				tokenString.Append(m_currentChar);
				readNextChar();
			} while( isLETTER() || isDIGIT() );
			
			Token.TokenType tokenType = Token.TokenType.NAME;
			
			if(tokenString.ToString() == "if") {
				tokenType = Token.TokenType.IF;
			}
			else if(tokenString.ToString() == "else") {
				tokenType = Token.TokenType.ELSE;
			}
			else if(tokenString.ToString() == "return") {
				tokenType = Token.TokenType.RETURN;
			}
			
			return new Token(tokenType, tokenString.ToString());
		}
		
		private Token NUMBER(bool negative) {
			StringBuilder tokenString = new StringBuilder();
			if(negative) { tokenString.Append("-"); }
			bool period = false;
			do {
				if (m_currentChar == '.' && !period) {
					tokenString.Append(".");
					readNextChar();
				} else if (m_currentChar == '.' && period) {
					m_errorHandler.errorOccured("Can't have several period signs in a number!", Error.ErrorType.SYNTAX, m_currentLine, m_currentPosition);
				}
				tokenString.Append(m_currentChar);
				readNextChar();
			} while( isDIGIT() || m_currentChar == '.' );
			
			return new Token(Token.TokenType.NUMBER, tokenString.ToString());
		}
		
		private bool isLETTER() {
			
			foreach(char letter in s_letters) {
				if(m_currentChar == letter) return true;
			}
			return false;
		}
		
		private bool isDIGIT() {
			
			foreach(char digit in s_digits) {
				if(m_currentChar == digit) return true;
			}
			return false;
		}
		
		void readNextChar() {
			
		    int c = m_textReader.Read();
		    if (c > 0) {
		        m_currentChar = (char)c;
				m_currentPosition++;
			}
		    else {
				m_currentChar = '\0';
				m_endOfFile = true;
			}
		}
		
		List<Token> m_tokens = new List<Token>();
		TextReader m_textReader;
		
		bool m_processed = false;
		bool m_endOfFile;
		char m_currentChar;
		int m_currentLine;
		int m_currentPosition;
		int m_currentTokenStartPosition;
		
		static string s_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
		static string s_digits = "1234567890";
		
		ErrorHandler m_errorHandler;
	}
}
